//
// Created by tang on 22-10-25.
//

#include "interpolation.hpp"
#include "imgui/ImCurveEdit.h"
#include "utils/path_utils.hpp"

static GLfloat all_nan[8192 * 4]{ 0.f };// 4: RGBA

static std::vector<std::pair<GLfloat, GLfloat>> ReadData(const std::string &filename) {
    auto path = std::filesystem::path{ Utils::GetDataDirPath() } / filename;
    std::ifstream ifs(path);
    std::vector<std::pair<GLfloat, GLfloat>> result;
    if (ifs.is_open()) {
        std::string line_content;
        while (std::getline(ifs, line_content, '\n')) {
            if (line_content.empty() || line_content.front() == ';')
                continue;

            auto colon_pos = line_content.find(':');
            if (colon_pos != std::string::npos) {
                float x, y;
                try {
                    x = std::stof(line_content.substr(0, colon_pos));
                    y = std::stof(line_content.substr(colon_pos + 1));
                    result.emplace_back(std::make_pair(x, y));
                } catch (std::exception &e) {
                    LOG_E("Error: %s", e.what());
                }
            }
        }
    }
    return result;
}

InterpolationApp::InterpolationApp(GLFWwindow *win, const char *name) : App(win, name), m_data_changed_(false), m_interpolation_algorithm_(0), m_drag_(win) {
    if (!std::isnan(all_nan[0])) {
        for (auto &x: all_nan) {
            x = std::numeric_limits<GLfloat>::quiet_NaN();
        }
    }
    m_axis_ranges_ = { -1.f, 1.f, -10.f, 10.f };

    m_drag_.SetActions(
            nullptr,
            [this](double dx, double dy) {
//                LOG_I("Primary: %.2f, %.2f", dx, dy);
                const float width = m_axis_ranges_.y - m_axis_ranges_.x;
                const float height = m_axis_ranges_.w - m_axis_ranges_.z;

                const float xdiff = (float)dx / (float)m_window_width_ * width;
                const float ydiff = (float)dy / (float)m_window_height_ * height;

                m_axis_ranges_ += glm::vec4(-xdiff,-xdiff,ydiff,ydiff);
            });
}

InterpolationApp::~InterpolationApp() = default;

void InterpolationApp::OnInitialize() {
    App::OnInitialize();

    m_interpolation_ = GL::Program::Create("data_process");
    m_vao1_ = GL::VertexArray::Create(1);

    m_vao1_->Bind();
    {
        auto buff = m_vao1_->Get(0);
        buff->Bind(GL_ARRAY_BUFFER);
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GL_FLOAT), (void *) 0);
        glEnableVertexAttribArray(0);
    }
    m_vao1_->UnBind();

    m_wave_ = GL::Program::Create("wave");
    m_vao2_ = GL::VertexArray::Create(1);

    m_vao2_->Bind();
    {
        auto buff = m_vao2_->Get(0);
        buff->Bind(GL_ARRAY_BUFFER);
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GL_FLOAT), (void *) 0);
        glEnableVertexAttribArray(0);
    }
    m_vao2_->UnBind();

    m_points_ = ReadData("waves.txt");
    m_data_changed_ = true;
}
void InterpolationApp::OnCleanup() {
    App::OnCleanup();
}

static const char *algorithm_names[]{
    "Linear",
    "Nearest"
};

void InterpolationApp::RenderConfigUI() {
    App::RenderConfigUI();

    if (ImGui::TreeNodeEx("插值设定", ImGuiTreeNodeFlags_Framed)) {
        ImGui::AlignTextToFramePadding();
        ImGui::TextUnformatted("插值法：");
        ImGui::SameLine();
        if (ImGui::BeginCombo("##Algorithm", algorithm_names[m_interpolation_algorithm_])) {
            for (int i = 0; i < std::size(algorithm_names); ++i) {
                if (ImGui::Selectable(algorithm_names[i])) {
                    m_interpolation_algorithm_ = i;
                    m_data_changed_ = true;
                }
                if (i == m_interpolation_algorithm_)
                    ImGui::SetItemDefaultFocus();
            }
            ImGui::EndCombo();
        }

        ImGui::TextUnformatted("画布设置 " ICON_FA_HAND_O_DOWN);
        if (ImGui::IsItemHovered())
            ImGui::SetTooltip("结构为{x,y,z,w} => {xMin, xMax, yMin, yMax}");
        auto const &v = m_axis_ranges_;
        m_data_changed_ |= ImGui::Vector4Ranged(
                "Axis Ranges",
                m_axis_ranges_,
                glm::vec4(-1000.f, v.x + .1f, -1000.f, v.z + .1f),// min
                glm::vec4(v.y - .1f, 1000.f, v.w - .1f, 1000.f),// max
                glm::vec4(-1.f, 1.f, -10.f, 10.f),// reset
                0.05f);
        if (ImGui::Button("数据编辑"))
            ImGui::OpenPopup("##DataEditor");
        DrawDataEditor();
        ImGui::SameLine();
        if (ImGui::Button("自适应"))
            FitDataRange();
        ImGui::TreePop();
    }
}

void InterpolationApp::RenderFrame(double delta_time, double total_time) {

    if (m_data_changed_) {

        const auto begin_time = std::chrono::high_resolution_clock::now();

        if (m_points_.size() >= 2) {
            std::vector<glm::vec4> data(m_points_.size());

            auto it2 = begin(m_points_);
            auto it1 = it2++;

            glm::vec4 *pData = data.data();
            pData->z = pData->w = std::numeric_limits<GLfloat>::quiet_NaN();
            pData->x = it1->first;
            pData->y = it1->second;

            ++pData;

            do {
                pData->z = it1->first;
                pData->w = it1->second;
                pData->x = it2->first;
                pData->y = it2->second;
                ++it2;
                ++it1;
                ++pData;
            } while (it2 != end(m_points_));

            auto buff = m_vao1_->Get(0);
            buff->Bind(GL_ARRAY_BUFFER);
            buff->Data(GL_ARRAY_BUFFER, data.size() * sizeof(glm::vec4), data.data(), GL_DYNAMIC_DRAW);
        }

        GLuint fbo;
        GLCallValidation(glGenFramebuffers(1, &fbo));
        GLCallValidation(glBindFramebuffer(GL_FRAMEBUFFER, fbo));

        GLuint tex;
        GLCallValidation(glGenTextures(1, &tex));
        GLCallValidation(glBindTexture(GL_TEXTURE_2D, tex));
        GLCallValidation(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, m_window_width_, 1, 0, GL_RGBA, GL_FLOAT, all_nan));

        GLCallValidation(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
        GLCallValidation(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST));

        GLCallValidation(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0));
        GLenum DrawBuffers[2]{ GL_COLOR_ATTACHMENT0 };
        GLCallValidation(glDrawBuffers(1, DrawBuffers));

        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
            LOG_E("Error !");
            return;
        }

        GLCallValidation(glViewport(0, 0, m_window_width_, 1));

        GLCallValidation(glDisable(GL_DEPTH_TEST));

        m_interpolation_->Use();
        m_interpolation_->Uniform2f("xRange", glm::vec2(m_axis_ranges_));
        m_interpolation_->Uniform1i("algorithm", m_interpolation_algorithm_);
        m_vao1_->Bind();
        GLCallValidation(glDrawArrays(GL_LINE_STRIP, 0, m_points_.size()));

        {
            m_interpolation_data_.resize(m_window_width_ * 4);
            GLCallValidation(glReadPixels(0, 0, m_window_width_, 1, GL_RGBA, GL_FLOAT, &m_interpolation_data_[0]));
            GLCallValidation(glBindFramebuffer(GL_FRAMEBUFFER, 0));
            GLCallValidation(glDeleteFramebuffers(1, &fbo));
            GLCallValidation(glDeleteTextures(1, &tex));

            auto buff = m_vao2_->Get(0);
            buff->Bind(GL_ARRAY_BUFFER);
            buff->Data(GL_ARRAY_BUFFER, sizeof(GLfloat) * m_interpolation_data_.size(), m_interpolation_data_.data(), GL_DYNAMIC_DRAW);
        }

        m_vao1_->UnBind();
        m_interpolation_->NotUse();
        GLCallValidation(glViewport(0, 0, m_window_width_, m_window_height_));

        m_data_changed_ = false;

        const auto end_time = std::chrono::high_resolution_clock::now();

        const double us_time = std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - begin_time).count() / 1000.0;

        LOG_I("input size: %u, output size: %u, duration: %.3f us", m_points_.size(), (size_t) m_window_width_, us_time);
    }

    GLCallValidation(glEnable(GL_DEPTH_TEST));
    GLCallValidation(glEnable(GL_BLEND));

    GLCallValidation(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

    //    GLCallValidation(glEnable(GL_POINT_SMOOTH));
    //    GLCallValidation(glEnable(GL_LINE_SMOOTH));
    //    GLCallValidation(glEnable(GL_POLYGON_SMOOTH));

    m_wave_->Use();

    m_wave_->Uniform4f("viewRect", m_axis_ranges_);
    m_wave_->Uniform3f("curveColor", glm::vec3(1.f, 1.f, 1.f));
    GLCallValidation(glLineWidth(1.f));
    //    GLCallValidation(glPointSize(2.f));

    m_vao2_->Bind();
    GLCallValidation(glDrawArrays(GL_LINE_STRIP, 0, m_window_width_));
    //    GLCallValidation(glDrawArrays(GL_POINTS, 0, m_window_width_));
    m_vao2_->UnBind();

    // draw raw point
    m_wave_->Uniform3f("curveColor", glm::vec3(1.f, 0.f, 0.f));
    m_vao1_->Bind();
    GLCallValidation(glDrawArrays(GL_POINTS, 0, m_points_.size()));
    m_vao1_->UnBind();

    m_wave_->NotUse();
}

void InterpolationApp::OnWindowResized(int w, int h) {
    App::OnWindowResized(w, h);
    m_data_changed_ = true;
}

void InterpolationApp::FitDataRange() {
    glm::vec4 result{ 1e20f, -1e20f, 1e20f, -1e20f };

    for (auto const &data: m_points_) {
        result.x = std::min<float>(result.x, data.first);
        result.y = std::max<float>(result.y, data.first);
        result.z = std::min<float>(result.z, data.second);
        result.w = std::max<float>(result.w, data.second);
    }

    float width = result.y - result.x;
    float height = result.w - result.z;

    m_axis_ranges_ = result + glm::vec4(-0.01f * width, 0.01f * width, -0.01f * height, 0.01f * height);

    m_data_changed_ = true;// 重新插值
}
void InterpolationApp::DrawDataEditor() {
    if (ImGui::BeginPopupModal("##DataEditor", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        struct MyDelegate : ImCurveEdit::Delegate {
            explicit MyDelegate(std::vector<std::pair<GLfloat, GLfloat>> &points) : m_points_(points) { }

            ImCurveEdit::CurveType GetCurveType(size_t curveIndex) const override {
                return ImCurveEdit::CurveLinear;
            }
            unsigned int GetBackgroundColor() override {
                return 0xFFFFFFFF;
            }

            size_t GetCurveCount() override {
                return 1;
            }
            ImVec2 &GetMin() override {
                return min;
            }
            ImVec2 &GetMax() override {
                return max;
            }
            size_t GetPointCount(size_t curveIndex) override {
                return m_points_.size();
            }
            uint32_t GetCurveColor(size_t curveIndex) override {
                return 0xFFFF0000;
            }
            ImVec2 *GetPoints(size_t curveIndex) override {
                return (ImVec2 *) &m_points_[curveIndex];
            }
            int EditPoint(size_t curveIndex, int pointIndex, ImVec2 value) override {
                const size_t size = m_points_.size();

                if (size == 1) {
                    m_points_[curveIndex] = std::make_pair(value.x, value.y);
                } else {
                    if (pointIndex == 0) {
                        m_points_[pointIndex] = std::make_pair(std::min<float>(m_points_[pointIndex + 1].first - 0.005f, value.x), value.y);
                    } else if (pointIndex == size - 1) {
                        m_points_[pointIndex] = std::make_pair(std::max<float>(m_points_[pointIndex - 1].first + 0.005f, value.x), value.y);
                    } else {
                        const float x = std::clamp<float>(value.x, m_points_[pointIndex - 1].first - 0.005f, m_points_[pointIndex + 1].first + 0.005f);
                        m_points_[pointIndex] = std::make_pair(x, value.y);
                    }
                }

                return pointIndex;
            }
            void AddPoint(size_t curveIndex, ImVec2 value) override {
                m_points_.emplace_back(std::make_pair(value.x, value.y));
                std::sort(begin(m_points_), end(m_points_), [](auto a, auto b) { return a.first < b.first; });
            }

            std::vector<std::pair<GLfloat, GLfloat>> &m_points_;
            ImVec2 min{ -2, -10 }, max{ 2, 10 };
        };

        static MyDelegate data(m_points_);

        ImCurveEdit::Edit(data, ImVec2(500, 350), ImGui::GetID(this));

        if (ImGui::Button("关闭")) {
            ImGui::CloseCurrentPopup();
            m_data_changed_ = true;
        }
        ImGui::EndPopup();
    }
}
void InterpolationApp::OnMouseClicked(int button, int mods, bool is_press) {
    App::OnMouseClicked(button, mods, is_press);
    if (is_press)
        m_drag_.InjectPress(button == GLFW_MOUSE_BUTTON_LEFT);
    else
        m_drag_.InjectRelease();
}
void InterpolationApp::OnMouseMove(double x, double y) {
    App::OnMouseMove(x, y);
    m_drag_.InjectPosition(x, y);
}

DEFINE_FACTORY_FUNC(CreateInterpolationApp, win, args) {
    (void) args;
    return std::make_unique<InterpolationApp>(win, "Data Interpolation");
}

REGISTER_APP_CREATOR("InterpolationApp", &CreateInterpolationApp);